package edu.hawaii.nearestneighbor.classifiers;

import java.util.ArrayList;
import java.util.List;
import edu.hawaii.nearestneighbor.classifications.Classification;
import edu.hawaii.nearestneighbor.util.DataPoint;
import edu.hawaii.nearestneighbor.util.Gamma;
import edu.hawaii.nearestneighbor.util.NNutil;
import edu.hawaii.nearestneighbor.util.Parameters;

/**
 * Provides the implementation of the cam-weighted classification algorithm.
 * 
 * @author Robert Puckett
 * 
 */
public class CamClassifier extends KNNClassifier {

  /**
   * Debug level choice for method of determining cam distance.
   */
  private static final int camDistChoice = 2;

  /**
   * Parameter array derived from training.
   */
  private List<Parameters> A;

  /**
   * Contains the dimension of the random vector.
   */
  public int p;

  /**
   * Contains Constant #1 used in cam distribution.
   */
  public double c1;

  /**
   * Contains Constant #2 used in cam distribution.
   */
  public double c2;

  /**
   * Trains the internal A parameter list using the set of points specified.
   * 
   * @param points a set of Datapoints.
   */
  public void train(List<DataPoint> points) {
    this.A = trainParameters(points, k);
  }

  /**
   * Calculates constant 2 based upon the number of parameters.
   * 
   * @param p count of the number of parameters.
   * @return constant 2.
   */
  public static Double calcConstant2(int p) {
    return Math.sqrt(2) * (Gamma.gamma(((double) p + 1.0) / 2.0) / Gamma.gamma((double) p / 2.0));
  }

  /**
   * Calculates constant 1 based upon the number of parameters.
   * 
   * @param p count of the number of parameters.
   * @return constant 1.
   */
  public static Double calcConstant1(int p) {
    return (Math.sqrt(2) * (Gamma.gamma(((double) p + 1.0) / 2.0) / Gamma.gamma((double) p / 2.0)))
        / (double) p;
  }

  /**
   * Calculates constant 1 based upon the number of parameters and constant 2.
   * 
   * @param c2 constant 2 (saves computation).
   * @param p count of the number of parameters.
   * @return constant 1.
   */
  public static Double calcConstant1(double c2, int p) {
    return c2 / (double) p;
  }

  /**
   * Calculates the W matrix between a set of datapoints and a data point.
   * 
   * @param near the set of nearest data points to x.
   * @param x the data point being compared.
   * @return the matrix W.
   */
  public static List<List<Double>> calcW(List<DataPoint> near, DataPoint x) {
    // 3. Obtain W_i from V_i according to:
    //
    // w_ij = v_ij if y_ij = y_i0
    // w_ij = -0.5*v_ij if y_ij <> y_i0

    List<List<Double>> W = new ArrayList<List<Double>>();
    List<Double> vi = new ArrayList<Double>();
    for (int i = 0; i < near.size(); i++) {
      vi.clear();

      if (x.state.equals(near.get(i).state)) {
        vi = NNutil.diff(near.get(i).x, x.x);
      }
      else {
        // w_ij = -0.5*v_ij if y_ij <> y_i0
        for (int j = 0; j < x.x.size(); j++) {
          vi.add(-0.5 * (near.get(i).x.get(j) - x.x.get(j)));
        }
      }
      W.add(new ArrayList<Double>(vi));
    }
    return W;
  }

  /**
   * Calculates the hat(g_i) value.
   * 
   * @param W the W matrix.
   * @param k the size of W, also the number of nearest nieghbors being used.
   * @return the hat(g_i) value.
   */
  public static List<Double> calcGhat(List<List<Double>> W, int k) {

    // 4. Calculate hat(G_i) and hat(L_i) with W_i using:
    //

    // hat(G_i) = sum_{j=1}^{k} v_ij / k

    List<Double> ghat = new ArrayList<Double>();
    double gi;

    for (int i = 0; i < W.get(0).size(); i++) {
      gi = 0.0;
      for (int j = 0; j < k; j++) {
        gi = gi + W.get(j).get(i);
      }
      gi = gi / k;
      ghat.add(gi);
    }
    return ghat;
  }

  /**
   * Calculates the hat(l_i) value for the matrix W and k parameters.
   * 
   * @param W the W matrix.
   * @param k the number of elements in w, also the number of nearest neighbors being used.
   * @return the double value representing hat(l_i).
   */
  public static Double calcLhat(List<List<Double>> W, int k) {

    // hat(L_i) = sum_{j=1}^{k} ||v_ij|| / k

    double lhat = 0.0;
    for (int i = 0; i < W.size(); i++) {
      lhat = lhat + (NNutil.magnitude(W.get(i)));
    }
    return lhat / (double) k;
  }

  /**
   * Calculates the unit vector tau in the direction of ghat.
   * 
   * @param ghat the direction vector.
   * @return the unit vector tau in the direction of ghat.
   */
  public static List<Double> calcTau(List<Double> ghat) {
    return NNutil.calcUnitVector(ghat);
  }

  /**
   * Trains the parameters for the cam distribution to be used for classification (must be called
   * before classification is run).
   * 
   * @param points The set of training data points (prototypes).
   * @param k the number of neighbors to include in the training process.
   * @return the trained list of parameters.
   */
  public static List<Parameters> trainParameters(List<DataPoint> points, int k) {
    List<Double> t = new ArrayList<Double>();
    List<List<Double>> W = new ArrayList<List<Double>>();
    List<DataPoint> near = new ArrayList<DataPoint>();
    List<Double> ghat = new ArrayList<Double>();
    double lhat = 0.0;
    double a = 0.0;
    double b = 0.0;
    // Storage of parameters needed to calculate cam distance
    List<Parameters> A = new ArrayList<Parameters>();
    // P is the dimension of the feature vector, assuming all feature vectors have equal size
    int p = points.get(0).x.size();

    double c2 = calcConstant2(p);
    double c1 = calcConstant1(c2, p);

    for (DataPoint x : points) {
      near.clear();
      W.clear();
      ghat.clear();
      t.clear();
      near = NNutil.findKNearest(points, x, k);
      W = calcW(near, x);
      ghat = calcGhat(W, k);
      lhat = calcLhat(W, k);
      a = lhat / c2; // hat(a_i) = hat(L_i) / c2
      b = NNutil.magnitude(ghat) / c1; // hat(b_i) = ||hat(G_i)|| / c1
      t = calcTau(ghat); // hat(tau_i) = hat(G_i) / ||hat(G_i)||
      A.add(new Parameters(a, b, new ArrayList<Double>(t))); // Save a_i, b_i, tau_i to A_i
    }
    return A;
  }

  /**
   * Trains the parameters for the cam distribution to be used for classification (must be called
   * before classification is run).
   * 
   * @param points The set of training data points (prototypes).
   * @param k the number of neighbors to include in the training process.
   * @param threshold the confidence threshold value.
   * @return the trained list of parameters.
   */
  public static List<Parameters> trainParameters(List<DataPoint> points, int k, double threshold) {
    List<Double> t = new ArrayList<Double>();
    List<List<Double>> W = new ArrayList<List<Double>>();
    List<DataPoint> near = new ArrayList<DataPoint>();
    List<Double> ghat = new ArrayList<Double>();
    double lhat = 0.0;
    double a = 0.0;
    double b = 0.0;
    // Storage of parameters needed to calculate cam distance
    List<Parameters> A = new ArrayList<Parameters>();
    // P is the dimension of the feature vector, assuming all feature vectors have equal size
    int p = points.get(0).x.size();

    double c2 = calcConstant2(p);
    double c1 = calcConstant1(c2, p);

    for (DataPoint x : points) {
      near.clear();
      W.clear();
      ghat.clear();
      t.clear();
      StatConfClassifier.DoubleValueHolder conf = new StatConfClassifier.DoubleValueHolder();
      near = StatConfClassifier.findKConfNearest(points, x, threshold, k, conf);
      W = calcW(near, x);
      ghat = calcGhat(W, near.size());
      lhat = calcLhat(W, near.size());
      a = lhat / c2; // hat(a_i) = hat(L_i) / c2
      b = NNutil.magnitude(ghat) / c1; // hat(b_i) = ||hat(G_i)|| / c1
      t = calcTau(ghat); // hat(tau_i) = hat(G_i) / ||hat(G_i)||
      A.add(new Parameters(a, b, new ArrayList<Double>(t))); // Save a_i, b_i, tau_i to A_i
    }
    return A;
  }
  
  // This method has been replaced by the static method below.
  // It is kept for sentimental... umm... I mean historical reasons.
  //
  // /**
  // * Calculates the cam-weighted distance between x and q.
  // * @param i the index to the prototype x.
  // * @param x
  // * @param q
  // * @return
  // */
  // public double camDist(int i, DataPoint x, DataPoint q) {
  // // CamDist(x_i,q) = ||q-x_i||/(a_i + b_i cos theta_i),
  // // where theta_i is the included angle of vectors q-x_i and tau_i
  // DataPoint difference = NNutil.diff(q,x);
  // double theta = calcIncludedAngle(difference.x, this.A.get(i).t);
  // return NNutil.euclideanDistance(x, q)/(this.A.get(i).a+ this.A.get(i).b*Math.cos(theta));
  // }
  //

  /**
   * Calculates the cam weighted distance using the parameter matrix passed from the i^th data point
   * in data, and the point q.
   * 
   * @param A a parameter list of length p (where p is the number of prototypes).
   * @param i an index representing which prototype to be used in the calculation.
   * @param data a list of all the prototypes in same order as when A was generated.
   * @param q the other datapoint in the distance calculation.
   * @return the cam-weighted distance between the i^th prototype and q.
   */
  public static double camDist(List<Parameters> A, int i, List<DataPoint> data, DataPoint q) {
    // CamDist(x_i,q) = ||q-x_i||/(a_i + b_i cos theta_i),
    // where theta_i is the included angle of vectors q-x_i and tau_i
    List<Double> x = data.get(i).x;
    List<Double> difference = NNutil.diff(q.x, x); 

    // Alternative 1: Use dotproduct and magnitude
    double diffMag = NNutil.magnitude(difference);
    double costheta = NNutil.dotProd(difference, A.get(i).t) / diffMag;
    costheta = 1.0; // workaround for better results
    double result = Math.abs(diffMag / (A.get(i).a + (A.get(i).b * costheta)));
    return result;
    // Alternative 2: Use cos of included angle (doubtful if works)
//    double theta = NNutil.calcIncludedAngle(x, A.get(i).t);
//    if (theta < 0) {
//      System.out.println("FIX IT!!!!!!!");
//      try {
//        System.in.read();
//      }
//      catch (IOException e) {
//        
//        e.printStackTrace();
//      }
//    }
////    double result2 = Math.abs(NNutil.magnitude(difference)
////        / (A.get(i).a + A.get(i).b * Math.cos(Math.toRadians(theta))));
//    double result2 = Math.abs(NNutil.magnitude(difference)
//        / (A.get(i).a + A.get(i).b * Math.cos(0*theta)));
//    return result2;
//
//    // Alternative 3: similar to 2
//    double cosTheta = NNutil.cosIncludedAngle(x, A.get(i).t);
//    double camWeight = 1.0 / (A.get(i).a + (A.get(i).b * cosTheta));
//    double result3 = Math.abs(NNutil.magnitude(difference) * camWeight);
//
////    System.out.println("Dist: " + result1 + "  " + result2 + "  " + result3);
//
//    if (camDistChoice == 1) {
//      return result1;
//    }
//    if (camDistChoice == 2) {
//      return result2;
//    }
//    if (camDistChoice == 3) {
//      return result3;
//    }
//
//    return 9999999;
  }

  // Phase 2: Classification

  // 7. Calculate the cam weighted distance from q to each prototype x_i according to:
  //
  // CamDist(x_i,q) = ||q-x_i||/(a_i + b_i cos theta_i),
  // where theta_i is the included angle of vectores q-x_i and tau_i
  //
  // 8. Find the nearest neighbor x* in D which satisfies:
  //
  // CamDist(x*,q) = min (x_i in D) CamDist(x_i,q).
  //
  // 9. Return the label y*, where y* is the class label of x*.

  /**
   * Finds the k nearest points in data to the test point.
   * 
   * @param A the parameter matrix.
   * @param data the set of points.
   * @param test the test point to find the nearest neighbors.
   * @param k the number of neighbors to find.
   * @return the list of k datapoints that are closest to test point.
   */
  public static List<DataPoint> findKNearest(List<Parameters> A, List<DataPoint> data,
      DataPoint test, int k) {
    List<DataPoint> result = new ArrayList<DataPoint>();
    //
    // // TreeMap will maintain datapoints in sorted order of their distance (key value)
    // TreeMap<Double, DataPoint> dist2point = new TreeMap<Double, DataPoint>();
    //
    // // Find all of the distances between the test point and the points list.
    // for (int i = 0; i < data.size(); i++) {
    // // Add the associated distance/DataPoint to the treemap sorted by distance
    // // camDist(List<Parameters> A, int i, List<DataPoint> data, DataPoint q) {
    //
    // dist2point.put(camDist(A, i, data, test), data.get(i));
    // }
    //
    // double first = 0.0;
    // for (int i = 0; i < k; i++) {
    // first = dist2point.firstKey();
    // result.add(dist2point.get(first));
    // dist2point.remove(first);
    // }
    List<Integer> mins = new ArrayList<Integer>();
    List<Double> distances = new ArrayList<Double>();

    boolean isFirst = true;

    for (int i = 0; i < data.size(); i++) {
      distances.add(camDist(A, i, data, test));
    }

    int minIndex = 0;
    for (int i = 0; i < k; i++) {
      isFirst = true;
      for (int j = 0; j < distances.size(); j++) {
        if (mins.contains(j)) {
          continue;
        }
        if (isFirst) {
          minIndex = j;
          isFirst = false;
        }
        if (distances.get(j) < distances.get(minIndex)) {
          minIndex = j;
        }
      }
      mins.add(minIndex);
    }

    for (Integer c : mins) {
      result.add(data.get(c));
    }
    // System.out.print("C: ");
    // for (DataPoint c : result) {
    // System.out.print(c.state);
    // }
    // System.out.println();
    return result;
  }

  /**
   * Classifies using the cam classification algorithm.
   * 
   * @param data set of all data points.
   * @param test the point to be classified.
   * @return the classification of test.
   * @see edu.hawaii.nearestneighbor.classifiers.KNNClassifier#Classify(java.util.List,
   *      edu.hawaii.nearestneighbor.util.DataPoint)
   */
  public Classification classify(List<DataPoint> data, DataPoint test) {
    // Find the k-nearest data points to the test point.
    List<DataPoint> kNearest = findKNearest(this.A, data, test, k);

    // Finds the most often occurence of the state in the nearest points.
    return new Classification(NNutil.modeState(kNearest), k);

  }

  // public Classification Classify(List<DataPoint> data, DataPoint test, int distancetype) {
  // // Find the k-nearest data points to the test point.
  // List<DataPoint> kNearest = findKNearest(this.A, data, test, k);
  //    
  // // Finds the most often occurence of the state in the nearest points.
  // return new Classification(NNutil.modeState(kNearest),k);
  // }

  /**
   * Classifies the test point using the parameters in A, finding the k cam-nearest points in data.
   * 
   * @param A the parameter list with the same number of parameters as points in data.
   * @param data the prototypes (training data).
   * @param test the point to be classified.
   * @param k the number of neighbors to use in the classification.
   * @return the classification of the test point using cam-weighted distance.
   */
  public static Classification classify(List<Parameters> A, List<DataPoint> data, DataPoint test,
      int k) {
    // Find the k-nearest data points to the test point.
    List<DataPoint> kNearest = CamClassifier.findKNearest(A, data, test, k);

    // Finds the most often occurence of the state in the nearest points.
    return new Classification(NNutil.modeState(kNearest), k);
  }

}
